package com.wideplay.warp.security;

import com.google.inject.Inject;
import com.google.inject.Injector;
import net.jcip.annotations.Immutable;
import net.jcip.annotations.NotThreadSafe;
import org.acegisecurity.context.HttpSessionContextIntegrationFilter;
import org.acegisecurity.intercept.web.FilterInvocationDefinitionSource;
import org.acegisecurity.intercept.web.FilterInvocationDefinitionSourceEditor;
import org.acegisecurity.intercept.web.FilterSecurityInterceptor;
import org.acegisecurity.providers.ProviderManager;
import org.acegisecurity.providers.anonymous.AnonymousAuthenticationProvider;
import org.acegisecurity.providers.anonymous.AnonymousProcessingFilter;
import org.acegisecurity.providers.dao.DaoAuthenticationProvider;
import org.acegisecurity.ui.AccessDeniedHandlerImpl;
import org.acegisecurity.ui.ExceptionTranslationFilter;
import org.acegisecurity.ui.logout.LogoutFilter;
import org.acegisecurity.ui.logout.LogoutHandler;
import org.acegisecurity.ui.logout.SecurityContextLogoutHandler;
import org.acegisecurity.ui.webapp.AuthenticationProcessingFilter;
import org.acegisecurity.ui.webapp.AuthenticationProcessingFilterEntryPoint;
import org.acegisecurity.userdetails.memory.UserAttribute;
import org.acegisecurity.userdetails.memory.UserAttributeEditor;
import org.acegisecurity.vote.AffirmativeBased;
import org.acegisecurity.vote.AuthenticatedVoter;
import org.acegisecurity.vote.RoleVoter;
import org.acegisecurity.wrapper.SecurityContextHolderAwareRequestFilter;

import javax.servlet.Filter;
import javax.servlet.ServletException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Created with IntelliJ IDEA.
 * On: 16/10/2007
 *
 * This builder does not modify any internal state (only reads it), so is semantically threadsafe.
 * However, immutability of its member "config" cannot be guaranteed. So it is marked as *not* threadsafe.
 * This builder is intended to be used under confinement and never concurrently.
 *
 * @author Dhanji R. Prasanna
 * @since 1.0
 */
@NotThreadSafe
class SecurityConfigurationBuilder {
    private final SimpleConfig config;
    private final Injector injector;

    private static final String J_ACEGI_SECURITY_CHECK = "/j_acegi_security_check";
    private static final String ANON_KEY = "anonKey";
    private static final String USER_ATTR_ANONYMOUS = "guest,ROLE_ANONYMOUS";

    @Inject
    SecurityConfigurationBuilder(SimpleConfig config, Injector injector) {
        this.config = config;
        this.injector = injector;
    }

    public List<Filter> build() throws ServletException {
        List<Filter> filterChain = new ArrayList<Filter>();

        //add the session context integrator first (it loads up any existing session and merges with acegi context)
        filterChain.add(injector.getInstance(HttpSessionContextIntegrationFilter.class));

        //add the logout filter to listen for logouts and handle them (by default at /j_acegi_logout )
        filterChain.add(logoutFilter(filterChain));

        //add the authentication processing filter and its dependent services
        final AuthenticationManagerResult authenticationManagerResult = authenticationProcessingFilter(); //also creates the authManager
        filterChain.add(authenticationManagerResult.authenticationProcessingFilter);

        //add the security context holder aware request filter (wraps the request in a wrapper that loads a saved request)
        filterChain.add(injector.getInstance(SecurityContextHolderAwareRequestFilter.class));

        //the anonymous processing filter is next
        filterChain.add(anonymousProcessingFilter());

        //add an exceptiontranslation filter that handles Access denied and auth failed cases thrown by inner filters
        filterChain.add(exceptionTranslationFilter());

        //finally add the authorization filter (that secures urls and resources)
        filterChain.add(filterInvocationInterceptor(authenticationManagerResult.authenticationManager));


        return filterChain;
    }

    private FilterSecurityInterceptor filterInvocationInterceptor(ProviderManager authenticationManager) {
        FilterSecurityInterceptor interceptor = injector.getInstance(FilterSecurityInterceptor.class);
        interceptor.setAuthenticationManager(authenticationManager);


        AffirmativeBased accessDecisionManager = injector.getInstance(AffirmativeBased.class);
        accessDecisionManager.setDecisionVoters(Arrays.asList(
                //all these votes must pass for an authorization to succeed
                injector.getInstance(RoleVoter.class),
                injector.getInstance(AuthenticatedVoter.class)
        ));
        interceptor.setAccessDecisionManager(accessDecisionManager);

        //build an authorization config string based on the fluent config
        StringBuilder builder = new StringBuilder();
        builder.append("PATTERN_TYPE_APACHE_ANT");
        builder.append(" \n ");

        for (SimpleConfig.AuthorizationEntry entry : config.getAuthorizations()) {
            builder.append(entry.getAntUrlPattern());
            builder.append("=");

            for (String role : entry.getRoles()) {
                builder.append(role);
                builder.append(",");
            }

            //chew up last comma
            builder.deleteCharAt(builder.length() - 1);
            
            builder.append(" \n ");
        }
        //set via property editor
        FilterInvocationDefinitionSourceEditor definitionSourceEditor = injector.getInstance(FilterInvocationDefinitionSourceEditor.class);
        definitionSourceEditor.setAsText(builder.toString());
        interceptor.setObjectDefinitionSource((FilterInvocationDefinitionSource) definitionSourceEditor.getValue());

        return interceptor;
    }

    private ExceptionTranslationFilter exceptionTranslationFilter() {
        ExceptionTranslationFilter exceptionTranslationFilter = injector.getInstance(ExceptionTranslationFilter.class);

        //talks about how we enter the webapp
        AuthenticationProcessingFilterEntryPoint point = injector.getInstance(AuthenticationProcessingFilterEntryPoint.class);
        point.setForceHttps(false);
        point.setLoginFormUrl(config.getLogoutUrl());   /** where do we login from? **/
        
        exceptionTranslationFilter.setAuthenticationEntryPoint(point);


        
        //talk about access denied (authz failures)
        AccessDeniedHandlerImpl deniedHandler = injector.getInstance(AccessDeniedHandlerImpl.class);

        //use the failure url if a deny url is not configured
        String denyUrl = config.getDenyUrl();
        if (null == denyUrl)
            denyUrl = config.getFailUrl();
        deniedHandler.setErrorPage(denyUrl);

        exceptionTranslationFilter.setAccessDeniedHandler(deniedHandler);

        return exceptionTranslationFilter;
    }

    private AnonymousProcessingFilter anonymousProcessingFilter() {
        AnonymousProcessingFilter filter = injector.getInstance(AnonymousProcessingFilter.class);
        filter.setKey(getAnonKey());

        UserAttributeEditor attributeEditor = injector.getInstance(UserAttributeEditor.class);
        attributeEditor.setAsText(USER_ATTR_ANONYMOUS);

        filter.setUserAttribute((UserAttribute) attributeEditor.getValue());
        return filter;
    }

    private LogoutFilter logoutFilter(List<Filter> filterChain) {
        return new LogoutFilter(config.getLogoutUrl(), /** url sent to after logging out **/
                new LogoutHandler[] { injector.getInstance(SecurityContextLogoutHandler.class) } /** disposes the acegi security context **/
        );
    }

    private AuthenticationManagerResult authenticationProcessingFilter() {
        AuthenticationProcessingFilter authenticationProcessingFilter = injector.getInstance(AuthenticationProcessingFilter.class);
        authenticationProcessingFilter.setDefaultTargetUrl(config.getSuccessUrl());
        authenticationProcessingFilter.setFilterProcessesUrl(J_ACEGI_SECURITY_CHECK);
        authenticationProcessingFilter.setAuthenticationFailureUrl(config.getFailUrl());

        //add an authentication manager (that uses the user details service)
        ProviderManager authenticationManager = injector.getInstance(ProviderManager.class);
        authenticationManager.setProviders(Arrays.asList(
                //add authenticator providers as required here (example LDAP)
                //...
                userDao(), //add a custom Dao authenticator
                anonymous() //failing which an anonymous auth
        ));

        //add it to the auth processing filter
        authenticationProcessingFilter.setAuthenticationManager(authenticationManager);

        return new AuthenticationManagerResult(authenticationManager, authenticationProcessingFilter);
    }

    @Immutable
    private static class AuthenticationManagerResult {
        private final ProviderManager authenticationManager;
        private final AuthenticationProcessingFilter authenticationProcessingFilter;

        public AuthenticationManagerResult(ProviderManager authenticationManager, AuthenticationProcessingFilter authenticationProcessingFilter) {
            this.authenticationManager = authenticationManager;
            this.authenticationProcessingFilter = authenticationProcessingFilter;
        }
    }

    /**
     *
     * @return Returns an anonymous authentication provider configured with the specified
     *  anonymity key.
     */
    private AnonymousAuthenticationProvider anonymous() {
        AnonymousAuthenticationProvider provider = injector.getInstance(AnonymousAuthenticationProvider.class);
        provider.setKey(getAnonKey());

        return provider;
    }

    private String getAnonKey() {
        String anonKey = config.getAnonymousKey();

        if (null == anonKey)
            anonKey = ANON_KEY;
        return anonKey;
    }

    /**
     * Expect UserDetailsService to be a singleton (typically).
     *
     * @return Returns a DaoAuthenticationProvider configured with an instance of UserDetailsService
     *  specified in the original configuration of Warp.
     */
    private DaoAuthenticationProvider userDao() {
        DaoAuthenticationProvider provider = injector.getInstance(DaoAuthenticationProvider.class);
        provider.setUserDetailsService(injector.getInstance(config.getUserDetailsService()));
        return provider;
    }
}
